Dependency Injection
Dependency Inversion Principle
Uncle BobのSOLID原則の1つ。
抽象は詳細に依存すべきではない。
コードは、抽象化のレベルが同じかそれより高いものに依存する。
もたらされるベネフィット
"Dependency Injection Principles, Practices, and Patterns"の中では、次の5つがDIPの利点であるとされている。
遅延バインディング
コードを再コンパイルすることなく、あるコンポーネントを入れ替えることができる。
エンタープライズアプリケーションでは実行環境がカチッと定まっているので、遅延バインディングによってもたらされる恩恵はあまりない。
拡張性
コードを拡張して再利用できる。
(これはOCPの原則も同時にまもらないとダメ)
平行開発
インタフェースにしか依存しないので、コンポーネントの実装は平行して開発できる。
保守性
インタフェースによって責務が規定されるので、メンテナンスはしやすくなる。
テスト容易性
実装を差し替えられるのでテストがしやすくなる。
依存関係の分類
Stable Dependencies
クラスやモジュールが既に存在している
想定しうる変更が、破壊的変更を含むことはない
問題の型は、決定論的アルゴリズムを含んでいる
クラスやモジュールを別のものに置き換えたり、ラップしたり、デコレートしたり、インターセプトしたりする必要がない。
Volatile Dependencies
依存関係がアプリケーションの実行環境をセットアップして設定を要求する。
依存関係がまだ存在しない、または開発中である。
依存関係が開発で使う全てのマシンにはインストールされない。
依存関係が非決定性の振る舞いを含む。
ランダム値や現在時刻の取得など
抽象がなければDIPは必要ない
DIのパターン
DIPを実現するもっともメジャーな手段がDependency Injection(以後DI)であり、依存をコンポーネントの外から渡すことである。DIを実現するうえで以下のパターンが存在する。
コンポジションルート Composition Root
オブジェクトグラフを構成する場所。
コンストラクタインジェクション Constructor Injection
必要な依存関係をクラスのコンストラクタのパラメータとして指定することで、それらを静的に定義する。
DIのデフォルトパターンであるべき。
Constrained Constructionアンチパターンを適用するフレームワークは、コンストラクタインジェクションを難しくするかもしれない。
メソッドインジェクション Method Injection
コンポジションルートの外側で呼び出されるメソッドの引数として、
適用できるところが限られる
依存関係がクラスやその抽象クラスのpublic APIの一部になってしまう
メソッドインジェクションを適用する典型的なパターンは、
呼び出し毎にインジェクション対象の依存コンポーネントが変わる。
プロパティインジェクション Property Injection
ローカルのデフォルト値をpublicのセット可能なプロパティを介して置き換えができる。別名セッターインジェクション。
依存コンポーネントが任意の場合に使う。必須なものであれば、コンストラクタインジェクションを使う。
時間的結合を引き起こす
アンチパターン
Control Freak
依存関係が直接コントロールされている。すなわち依存ライブラリを自分でnewしちゃうパターン。当然ながらすべてのDIPのメリットが無くなってしまうので、NG。
code:java
class ProductController {
public ModelAndView index() {
ProductService service = new ProductServiceImpl();
}
}
Service Locator
DIPは、DIだけが実現手段ではない。
Composit Rootの外から、依存するコンポーネントを探し出し返してくれるアプリケーションコンポーネントをService Locatorという。
code:java
class ProductController {
private final ApplicationContext context;
public ProductController(ApplicationContext context) {
this.context = context;
}
public ModelAndView index() {
ProductService service = context.getBean(ProductService.class);
}
}
コンポーネントがService Locatorに依存するので、再利用がしにくくなる。
コンストラクタインジェクションができるのであればそうする。
コンストラクタインジェクションは、そのコンポーネントが何に依存しているかをIDEが明示的に示してくれるから。
Ambient Context
staticなアクセッサを通じて利用される単一の強い型付けされた依存をAmbient Contextと呼ぶ。Ambient Contextは、シングルトンパターンと構造は似ているが、シングルトンと違い異なるオブジェクトが返されてもよい。
code:java
class ProductController {
public ModelAndView index() {
LocalDateTime.now();
}
}
Ambient Contextは、依存関係を見えにくくしてしまうことが問題である。
Constrained Constructor
ある抽象インタフェースを実装するオブジェクトすべてが、遅延バインディングを目的として、それらのコンストラクタが同一のシグネチャを持つことを強制される。
コードスメル
やりすぎたコンストラクタインジェクション
code:ValidatorImpl.java
public ValidatorImpl(ConstraintValidatorFactory constraintValidatorFactory,
BeanMetaDataManager beanMetaDataManager,
ValueExtractorManager valueExtractorManager,
ConstraintValidatorManager constraintValidatorManager,
ValidationOrderGenerator validationOrderGenerator,
ValidatorFactoryScopedContext validatorFactoryScopedContext) {
this.constraintValidatorFactory = constraintValidatorFactory;
this.beanMetaDataManager = beanMetaDataManager;
this.valueExtractorManager = valueExtractorManager;
this.constraintValidatorManager = constraintValidatorManager;
this.validationOrderGenerator = validationOrderGenerator;
this.validatorScopedContext = new ValidatorScopedContext( validatorFactoryScopedContext );
this.traversableResolver = validatorFactoryScopedContext.getTraversableResolver();
this.constraintValidatorInitializationContext = validatorFactoryScopedContext.getConstraintValidatorInitializationContext();
}
HibernateValidatorには、Over−Injection気味のコンポーネントが多い。例えば、このValidatorImplは6つのコンポーネントに依存している。ValidatorImplの責務は、「オブジェクトの中身をバリデーションする」であるが、
対策としては、振る舞いをファサードとして定義
Abstract Factoryの乱用
引数なしのCreateメソッドを呼ぶんだったら生成されたオブジェクトを渡そう。
実例
Spring
オブジェクトグラフの表現として、
XML
アノテーション
プログラマブル
が選択可能である。アノテーションベースではSpring以外でコンポーネントの再利用ができない(DIを別の仕組みで作る必要がある)が、それが主流である。
Constructor Injectionであれば、アノテーションベースでもアノテーション不要なので、極力そう設計することが推奨される。
Google Guice
プログラマブルタイプのDIコンテナである。
code:RealBillingService.java
public class RealBillingService implements BillingService {
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
CreditCardProcessor processor = new PaypalCreditCardProcessor();
TransactionLog transactionLog = new DatabaseTransactionLog();
try {
ChargeResult result = processor.charge(creditCard, order.getAmount());
transactionLog.logChargeResult(result);
return result.wasSuccessful()
? Receipt.forSuccessfulCharge(order.getAmount())
: Receipt.forDeclinedCharge(result.getDeclineMessage());
} catch (UnreachableException e) {
transactionLog.logConnectException(e);
return Receipt.forSystemFailure(e.getMessage());
}
}
}
Guiceでのコンポーネント定義は、抽象型名に実装クラスを明示的にバインディングするシンプルなタイプ。
code:BillingModule.java
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class);
bind(BillingService.class).to(RealBillingService.class);
}
}
Rails
Rails、というかRubyのプロダクトではDIが使われることはあまりないようだ。
DIが持つテスタビリティのメリットは、DHHが述べている。
code:time.rb
Time.stub(:now) { Time.new(2012, 12, 24) }
article.publish!
assert_equal 24, article.published_at.day
Time.nowメソッドは、Ambient Contextであるが、テスト時に簡単に実装を置き換えることができるので、DIは要らないという話。
Railsの上で開発する際に、DIを意識することがないかもしれないが、フレームワーク内にはVolatile Dependenciesの箇所で、DIが使われている。
判断基準
DIすべきかどうか?
する
Volatile Dependency
開発・テストとプロダクションで使うプロダクトが異なる
開発を並行してやりたい。
テスト容易性
よくある事例は現在時刻を
IoCを実現する箇所
ドメイン層は永続層を使うが、永続層の実装に依存させない
根本は「テスト容易性」につながる。
しない
横断的関心事
Loggerや認証/認可、トランザクションなど
AOPやDecoratorで処理する
依存関係が適切か?
DIコンテナなしで、ユニットテストが可能か?
そうでない場合は、Over-Injectionのスメルなので、クラスを責務ごとに分割するか、ファサードを導入する。